/*
    libhdvbd - Library for building sophisticated DVB applications
    Copyright (C) 2012 Tony Houghton <h@realh.co.uk>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include <glib.h>
#include <glib-object.h>
#include <glib/gstdio.h>

#include <string.h>

#include "cacheddb.h"
#include "i18n.h"

static gboolean
open_ram_db(HdvbCachedDb *self, gboolean create_schemas)
{
    self->db = hdvb_db_open(":memory:", self->version,
            create_schemas ? self->creator : NULL, self->creator_handle);
    return self->db != NULL;
}

static gboolean
hdvb__cached_db_open(HdvbCachedDb *self)
{
    gboolean exists = g_file_test(self->filename, G_FILE_TEST_IS_REGULAR);
    sqlite3 *ondisc = NULL;
    gboolean result = TRUE;
    
    if (exists || !self->cache)
    {
        ondisc = hdvb_db_open(self->filename, self->version,
                self->creator, self->creator_handle);
        if (!ondisc)
            result = FALSE;
    }
    if (result && self->cache)
    {
        result = open_ram_db(self, ondisc == NULL);
        if (result && ondisc)
        {
            if (!HDVB_CACHED_DB_GET_CLASS(self)->copy_data(self,
                    ondisc, self->db))
            {
                sqlite3_close(self->db);
                result = open_ram_db(self, TRUE);
                result = FALSE;
            }
        }
    }
    else if (result && !self->cache)
    {
        self->db = ondisc;
        ondisc = NULL;
    }
    if (ondisc)
        sqlite3_close(ondisc);
    if (!result && self->db)
    {
        sqlite3_close(self->db);
        self->db = NULL;
    }
    return result;
}

/* Need a dummy creator otherwise hdvb_db_open won't create an empty file */
static void
dummy_creator(sqlite3 *sql, gpointer handle)
{
    (void) sql;
    (void) handle;
}

static gboolean
hdvb__cached_db_sync(HdvbCachedDb *self)
{
    char *tmp = self->filename;
    sqlite3 *db = NULL;
    gboolean result = TRUE;
    
    if (!self->cache)
    {
        return TRUE;
    }
    
    if (g_file_test(tmp, G_FILE_TEST_IS_REGULAR))
    {
        char *dot = strrchr(self->filename, '.');
        
        if (dot)
        {
            tmp = g_strdup_printf("%.*s.new%s", (int) (dot - self->filename),
                    self->filename, dot);
        }
        else
        {
            tmp = g_strdup_printf("%s.new", self->filename);
        }
        if (g_file_test(tmp, G_FILE_TEST_EXISTS))
            g_remove(tmp);
    }
    db = hdvb_db_open(tmp, self->version,
            self->create_schemas ? self->creator : dummy_creator,
            self->creator_handle);
    if (!db)
    {
        g_debug("Couldn't create %s to sync cached db", tmp);
        result = FALSE;
    }
    if (result && !HDVB_CACHED_DB_GET_CLASS(self)->copy_data(self,
            self->db, db))
    {
        result = FALSE;
    }
    if (db)
    {
        sqlite3_close(db);
        db = NULL;
    }
    if (tmp != self->filename)
    {
        if (result)
        {
            g_remove(self->filename);
            g_rename(tmp, self->filename);
        }
        else
        {
            g_remove(tmp);
        }
        g_free(tmp);
    }
    return result;
}
    
static gboolean
hdvb__cached_db_copy_data(HdvbCachedDb *self, sqlite3 *src, sqlite3 *dest)
{
    sqlite3_backup *backup;
    gboolean result = TRUE;
    
    if (src && dest)
        backup = sqlite3_backup_init(dest, "main", src, "main");
    else
        backup = NULL;
    if (backup)
    {
        int step_result;
        
        while ((step_result = sqlite3_backup_step(backup, -1)) == SQLITE_OK);
        result = step_result == SQLITE_DONE;
        sqlite3_backup_finish(backup);
    }
    else
    {
        result = FALSE;
    }
    if (!result)
    {
        char *basename = g_path_get_basename(self->filename);
        
        g_warning(_("Failed to transfer database '%s' between RAM and disc"),
                basename);
        g_free(basename);
    }
    return result;
}

G_DEFINE_TYPE(HdvbCachedDb, hdvb_cached_db, G_TYPE_OBJECT);

enum {
    PROP_0,
    
    PROP_FILENAME,
    PROP_CACHE,
    PROP_CREATE_SCHEMAS,
    PROP_CREATOR,
    PROP_CREATOR_HANDLE,
    PROP_VERSION
};

static void
hdvb_cached_db_set_property(GObject *object, guint property_id,
        const GValue *value, GParamSpec *pspec)
{
    HdvbCachedDb *self = HDVB_CACHED_DB(object);
    
    switch (property_id)
    {
        case PROP_FILENAME:
            g_free(self->filename);
            self->filename = g_strdup(g_value_get_string(value));
            break;
        case PROP_CACHE:
            self->cache = g_value_get_boolean(value);
            break;
        case PROP_CREATE_SCHEMAS:
            self->create_schemas = g_value_get_boolean(value);
            break;
        case PROP_CREATOR:
            self->creator = g_value_get_pointer(value);
            break;
        case PROP_CREATOR_HANDLE:
            self->creator_handle = g_value_get_pointer(value);
            break;
        case PROP_VERSION:
            self->version = g_value_get_int(value);
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
            break;
    }
}

static void
hdvb_cached_db_get_property(GObject *object, guint property_id,
        GValue *value, GParamSpec *pspec)
{
    HdvbCachedDb *self = HDVB_CACHED_DB(object);
    
    switch (property_id)
    {
        case PROP_FILENAME:
            g_value_set_string(value, self->filename);
            break;
        case PROP_CACHE:
            g_value_set_boolean(value, self->cache);
            break;
        case PROP_CREATE_SCHEMAS:
            g_value_set_boolean(value, self->create_schemas);
            break;
        case PROP_CREATOR:
            g_value_set_pointer(value, self->creator);
            break;
        case PROP_CREATOR_HANDLE:
            g_value_set_pointer(value, self->creator_handle);
            break;
        case PROP_VERSION:
            g_value_set_int(value, self->version);
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
            break;
    }
}

static void
hdvb_cached_db_init(HdvbCachedDb *self)
{
    self->filename = NULL;
    self->cache = TRUE;
    self->create_schemas = FALSE;
    self->creator = NULL;
    self->creator_handle = NULL;
}

static GObject *
hdvb_cached_db_constructor(GType gtype,
        guint n_properties, GObjectConstructParam *properties)
{
    GObjectClass *parent_class = G_OBJECT_CLASS(hdvb_cached_db_parent_class);
    GObject *obj = parent_class->constructor(gtype, n_properties, properties);
    HdvbCachedDb *self = HDVB_CACHED_DB(obj);
    
    HDVB_CACHED_DB_GET_CLASS(self)->db_open(self);
    return obj;
}

static void
hdvb_cached_db_dispose(GObject *gobj)
{
    HdvbCachedDb *self = HDVB_CACHED_DB(gobj);
    if (self->db)
    {
        hdvb_cached_db_sync(self);
        sqlite3_close(self->db);
        self->db = NULL;
    }
    G_OBJECT_CLASS(hdvb_cached_db_parent_class)->dispose(gobj);
}

static void
hdvb_cached_db_finalize(GObject *gobj)
{
    HdvbCachedDb *self = HDVB_CACHED_DB(gobj);
    
    g_free(self->filename);
    G_OBJECT_CLASS(hdvb_cached_db_parent_class)->finalize(gobj);
}

static void
hdvb_cached_db_class_init(HdvbCachedDbClass *klass)
{
    GObjectClass *gclass = G_OBJECT_CLASS(klass);
    GParamSpec *pspec;
    
    gclass->set_property = hdvb_cached_db_set_property;
    gclass->get_property = hdvb_cached_db_get_property;
    pspec = g_param_spec_string("filename",
            _("Filename"), _("Database's on-disc filename"),
            "database.sqlite",
            G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
    g_object_class_install_property(gclass,
            PROP_FILENAME, pspec);
    pspec = g_param_spec_boolean("cache",
            _("Cache"), _("Whether to cache database in memory"),
            TRUE,
            G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
    g_object_class_install_property(gclass,
            PROP_CACHE, pspec);
    pspec = g_param_spec_boolean("create-schemas",
            _("Create schemas"),
            _("Whether tables etc need to be created in an empty destination "
                    "before copying a database between memory and disc"),
            FALSE,
            G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
    g_object_class_install_property(gclass,
            PROP_CREATE_SCHEMAS, pspec);
    pspec = g_param_spec_pointer("creator",
            _("Creator"),
            _("HdvbCachedDbCreator function to create schemas"),
            G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
    g_object_class_install_property(gclass,
            PROP_CREATOR, pspec);
    pspec = g_param_spec_pointer("creator-handle",
            _("Creator handle"),
            _("Handle for creator"),
            G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
    g_object_class_install_property(gclass,
            PROP_CREATOR_HANDLE, pspec);
    pspec = g_param_spec_int("version",
            _("Version"),
            _("Database version"),
            G_MININT, G_MAXINT, 0,
            G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
    g_object_class_install_property(gclass,
            PROP_VERSION, pspec);
    
    gclass->constructor = hdvb_cached_db_constructor;
    gclass->dispose = hdvb_cached_db_dispose;
    gclass->finalize = hdvb_cached_db_finalize;
    
    klass->db_open = hdvb__cached_db_open;
    klass->sync = hdvb__cached_db_sync;
    klass->copy_data = hdvb__cached_db_copy_data;
}

HdvbCachedDb *
hdvb_cached_db_open(const char *filename,
        gboolean cache, gboolean create_schemas,
        HdvbCachedDbCreator creator, int version, gpointer creator_handle)
{
    GObject *obj = g_object_new(HDVB_TYPE_CACHED_DB,
            "filename", filename, "cache", cache,
            "create_schemas", create_schemas,
            "creator", creator,
            "creator-handle", creator_handle,
            "version", version,
            NULL);
    return HDVB_CACHED_DB(obj);
}

gboolean
hdvb_cached_db_sync(HdvbCachedDb *self)
{
    g_return_val_if_fail(HDVB_IS_CACHED_DB(self), FALSE);
    return HDVB_CACHED_DB_GET_CLASS(self)->sync(self);
}
